package naga.x.util;

import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;

public class LRUCache<K, V> implements Map<K, V> {
	
	private Map<K, V> map;
	private int capacity;
	private CacheListener<V> listener;
	private Lock lock;
	
	class InnerMap extends LinkedHashMap<K, V> {
		private static final long serialVersionUID = 1L;
		InnerMap(int initialCapacity, float loadFactor, boolean accessOrder) {
			super(initialCapacity, loadFactor, accessOrder);
		}
		@Override
		protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
			boolean remove = LRUCache.this.size() > LRUCache.this.capacity;
			CacheListener<V> lsr;
			if (remove && (lsr = LRUCache.this.listener) != null) {
				lsr.cacheEldestRemoved(eldest.getValue());
			}
			return remove;
		}
	}
	
	public LRUCache() {
		this(Integer.MAX_VALUE);
	}
	
	public LRUCache(int capacity) {
		this(capacity, null);
	}
	
	public LRUCache(int capacity, CacheListener<V> listener) {
		this(16, 0.75f, capacity, listener);
	}
	
	public LRUCache(int initialCapacity, float loadFactor, int capacity, CacheListener<V> listener) {
		this.map = new InnerMap(initialCapacity, loadFactor, true);
		this.capacity = capacity;
		this.listener = listener;
		this.lock = new ReentrantLock();
	}

	@Override
	public int size() {
		lock.lock();
		try {
			return map.size();
		} finally {
			lock.unlock();
		}
	}

	@Override
	public boolean isEmpty() {
		lock.lock();
		try {
			return map.isEmpty();
		} finally {
			lock.unlock();
		}
	}

	@Override
	public boolean containsKey(Object key) {
		lock.lock();
		try {
			return map.containsKey(key);
		} finally {
			lock.unlock();
		}
	}

	@Override
	public boolean containsValue(Object value) {
		lock.lock();
		try {
			return map.containsValue(value);
		} finally {
			lock.unlock();
		}
	}

	@Override
	public V get(Object key) {
		lock.lock();
		try {
			return map.get(key);
		} finally {
			lock.unlock();
		}
	}

	@Override
	public V put(K key, V value) {
		lock.lock();
		try {
			V oldVal = map.put(key, value);
			fireAdded(value);
			return oldVal;
		} finally {
			lock.unlock();
		}
	}

	@Override
	public V remove(Object key) {
		lock.lock();
		try {
			V val = map.remove(key);
			fireRemoved(val);
			return val;
		} finally {
			lock.unlock();
		}
	}

	@Override
	public void putAll(Map<? extends K, ? extends V> m) {
		lock.lock();
		try {
			map.putAll(m);
			fireAdded(m);
		} finally {
			lock.unlock();
		}
	}

	@Override
	public void clear() {
		lock.lock();
		try {
			fireRemoved(this);
			map.clear();
		} finally {
			lock.unlock();
		}
	}

	@Override
	public Set<K> keySet() {
		lock.lock();
		try {
			return map.keySet();
		} finally {
			lock.unlock();
		}
	}

	@Override
	public Collection<V> values() {
		lock.lock();
		try {
			return map.values();
		} finally {
			lock.unlock();
		}
	}

	@Override
	public Set<Entry<K, V>> entrySet() {
		lock.lock();
		try {
			return map.entrySet();
		} finally {
			lock.unlock();
		}
	}
	
	// thread safely
	@Override
	public void forEach(BiConsumer<? super K, ? super V> action) {
		lock.lock();
		try {
			Set<Entry<K, V>> entrys = map.entrySet();
			for (Entry<K, V> entry : entrys) {
				action.accept(entry.getKey(), entry.getValue());
			}
		} finally {
			lock.unlock();
		}
	}
	
	// can break the foreach
	public void forEach(MapProcedure<? super K, ? super V> procedure) {
		lock.lock();
		try {
			Set<Entry<K, V>> entrys = map.entrySet();
			for (Entry<K, V> entry : entrys) {
				if (!procedure.execute(entry.getKey(), entry.getValue())) {
					break;
				}
			}
		} finally {
			lock.unlock();
		}
	}

	private void fireAdded(Map<? extends K, ? extends V> map) {
		for (Entry<? extends K, ? extends V> entry : map.entrySet()) {
			fireAdded(entry.getValue());
		}
	}
	
	private void fireAdded(V value) {
		if (value != null && listener != null) {
			listener.cacheAdded(value);
		}
	}
	
	private void fireRemoved(Map<? extends K, ? extends V> map) {
		for (Entry<? extends K, ? extends V> entry : map.entrySet()) {
			fireRemoved(entry.getValue());
		}
	}
	
	private void fireRemoved(V value) {
		if (value != null && listener != null) {
			listener.cacheRemoved(value);
		}
	}

}
